<?php

namespace APP\Ctrl;
use CMS\Ctrl as Base;
use CMS\Helper as Helper;

class Forum extends Base {
	private $permissions = array();

	// construct
	public function run() {
		// set permissions
		$this->permissions();

		// manage uri paths
		if(count($this->uri) > 0) {
			@list($func, $id, $page) = $this->uri;
			$this->$func($id ? $id : '0-null', $page ? $page : 0);
		} else $this->index();
	}

	// permissions
	private function permissions() {
		// default
		$this->permissions = array(
			'root' => false,
			'view' => array(),
			'create' => array(),
			'moderate' => array()
		);

		// source from auth or guest
		if(isset($this->session->auth)) $source = $this->session->auth;
		else if(isset($this->session->guest)) $source = $this->session->guest;
		else $source = array();

		// set root permission
		if(isset($source['permissions']['root']))
			$this->permissions['root'] = (bool)$source['permissions']['root'];

		// set view permissions
		if(isset($source['permissions']['forum']['view']))
			$this->permissions['view'] = $source['permissions']['forum']['view'];

		// set view permissions
		if(isset($source['permissions']['forum']['create']))
			$this->permissions['create'] = $source['permissions']['forum']['create'];

		// set view permissions
		if(isset($source['permissions']['forum']['moderate']))
			$this->permissions['moderate'] = $source['permissions']['forum']['moderate'];
	}

	// rebuild search index
	public function rebuild() {
		// not logged in?
		if(!isset($this->session->auth))
			return $this->json = array('success' => false);

		// check for root perm
		if(!$this->permissions['root'])
			return $this->json = array('success' => false);

		// clear search index
		$this->db->exec("DELETE FROM words; DELETE FROM search; VACUUM;");

		// find all replies
		$result = $this->db->query("SELECT
			r.id, r.message, t.name

			FROM replies AS r
			LEFT JOIN topics AS t ON r.topic = t.id AND r.starter = 1");

		// transform
		while($res = $result->fetchArray(SQLITE3_ASSOC))
			$this->wordIndex($res['name'] . ' ' . $res['message'], $res['id']);

		// return success
		return $this->json = array('success' => true);
	}

	// get categories from index
	public function index() {
		$this->view->func = 'index';

		// query based on permissions
		$permissions = $this->db->escapeString('WHERE c.id IN (' . implode(',', $this->permissions['view']) . ')');

		// find the categories
		$result = $this->db->query("SELECT
			c.id, c.name, c.description, t.topics, t.posts,
			l.id AS last_id, l.name AS last_name, l.time AS last_time, l.concate_read, l.concate_time,
			u.id AS last_user, u.name AS last_uname, u.email AS last_email

			FROM categories AS c
			LEFT JOIN (SELECT COUNT(DISTINCT t.id) AS topics, COUNT(r.id) AS posts, t.category
				FROM topics AS t
				LEFT JOIN replies AS r ON t.id = r.topic
				GROUP BY t.category
			) AS t ON c.id = t.category
			LEFT JOIN (SELECT o.id, o.name, o.topic_category, o.time, o.author, GROUP_CONCAT(o.read, '+') AS concate_read, GROUP_CONCAT(o.time, '+') AS concate_time FROM (
				SELECT x.id, x.name, x.read, x.topic_category, x.time, x.author FROM (
					SELECT t.id, t.name, t.read, t.category AS topic_category, r.time, r.author
						FROM topics AS t
						LEFT JOIN replies AS r ON t.id = r.topic
						ORDER BY r.time ASC
					) AS x GROUP BY x.id ORDER BY x.time ASC
				) AS o GROUP BY o.topic_category
			) AS l ON c.id = l.topic_category
			LEFT JOIN users AS u ON l.author = u.id

		{$permissions}

		GROUP BY c.id
		ORDER BY c.position");

		// transform
		$data = array();
		while($res = $result->fetchArray(SQLITE3_ASSOC)) {
			$data[] = array(
				'name' => $res['name'],
				'link' => $res['id'] . '-' . Helper::generate_named_id($res['name']),
				'description' => $res['description'],
				'read' => $this->statusCategory($res['concate_read'], $res['concate_time']),
				'topics' => ($res['topics'] ? $res['topics'] : 0),
				'posts' => ($res['posts'] ? $res['posts'] : 0),
				'last_avatar' => $res['last_id'] ? md5(strtolower($res['last_email'])) : null,
				'last_user' => $res['last_id'] ? $res['last_user'] : null,
				'last_uname' => $res['last_id'] ? $res['last_uname'] : null,
				'last_name' => $res['last_id'] ? $res['last_name'] : null,
				'last_ulink' => $res['last_id'] ? $res['last_user'] . '-' . Helper::generate_named_id($res['last_uname']) : null,
				'last_link' => $res['last_id'] ? $res['last_id'] . '-' . Helper::generate_named_id($res['last_name']) : null,
				'last_time' => $res['last_id'] ? Helper::format_time($res['last_time']) : null
			);
		}

		// output data
		$this->view->data = $data;

		// set path
		$this->view->path = array(
			'func' => $this->view->func
		);
	}

	// decide if category is read or unread
	private function statusCategory($read, $time) {
		// guest ? don't bother
		if(!isset($this->session->auth))
			return true;

		// separate each topic
		$list_read = explode('+', $read);
		$list_time = explode('+', $time);

		// go through each topic
		$result = true;
		for($x = 0; $x < count($list_read); $x++)
			if($this->statusTopic($list_read[$x], $list_time[$x]) == false)
				$result = false;

		return $result;
	}

	// get category securly
	private function secureCategory($id) {
		// get category
		$category = $this->db->querySingle("SELECT id, name, type FROM categories WHERE id = '{$id}'", true);
		if(!$category)
			return false;

		// permission
		if(!isset($this->permissions['view'][$category['id']]))
			return false;

		// set category link
		$category['link'] = $category['id'] . '-' . Helper::generate_named_id($category['name']);

		// return category
		return $category;
	}

	// get topics from category
	public function category($id, $page) {
		$this->view->func = 'category';

		// prepare vars
		$id = explode("-", $id, 2);
		$id = $this->db->escapeString((int)reset($id));
		$page = $this->db->escapeString((int)$page);

		// get category
		$this->view->category = $this->secureCategory($id);
		if(!$this->view->category)
			die(new Error());

		// post permission
		$this->view->permission = isset($this->permissions['create'][$this->view->category['id']]);

		// count topics
		$count = $this->db->querySingle("SELECT COUNT(id) as nr FROM topics WHERE category = '{$id}'");
		$lastpage = ceil($count / $this->settings['nav_topics']);
		if($page < 1) $page = 1;
		if($page > $lastpage) $page = $lastpage;
		$this->view->pagination = Helper::paginate($lastpage, $page);
		$limit = $count ? ('LIMIT ' .($page - 1) * $this->settings['nav_topics'] .',' . $this->settings['nav_topics']) : null;

		// find the topics
		$result = $this->db->query("SELECT
			t.id, t.name, t.views, t.sticky, t.locked, t.type, t.read,
			r.replies, r.time AS last_time,
			a.id AS author_id, a.name As author_name,
			u.id AS last_id, u.email AS last_email, u.name As last_name

			FROM topics AS t
			LEFT JOIN (SELECT COUNT(x.id) AS replies, x.topic, x.author, x.time FROM (
				SELECT id, topic, author, time
					FROM replies
					ORDER BY time ASC
				) AS x GROUP BY x.topic
			) AS r ON t.id = r.topic
			LEFT JOIN users AS a ON t.author = a.id
			LEFT JOIN users AS u ON r.author = u.id

		WHERE t.category = '{$id}'
		GROUP BY t.id
		ORDER BY t.sticky DESC, r.time DESC {$limit}");

		// transform
		$data = array();
		$this->view->last_sticky = null;
		while($res = $result->fetchArray(SQLITE3_ASSOC)) {
			$data[] = array(
				'id' => $res['id'],
				'name' => $res['name'],
				'link' => $res['id'] . '-' . Helper::generate_named_id($res['name']),
				'author_name' => $res['author_name'],
				'author_link' => $res['author_id'] . '-' . Helper::generate_named_id($res['author_name']),
				'sticky' => $res['sticky'],
				'locked' => $res['locked'],
				'type' => $res['type'],
				'read' => $this->statusTopic($res['read'], $res['last_time']),
				'replies' => ($res['replies'] ? $res['replies'] - 1 : 0),
				'views' => ($res['views'] ? $res['views'] : 0),
				'last_avatar' => md5(strtolower($res['last_email'])),
				'last_user' => $res['last_id'],
				'last_name' => $res['last_name'],
				'last_link' => $res['last_id'] . '-' . Helper::generate_named_id($res['last_name']),
				'last_time' => Helper::format_time($res['last_time'])
			);

			// set last sticky topic
			if($res['sticky'] == 1)
				$this->view->last_sticky = $res['id'];
		}

		// output data
		$this->view->data = $data;

		// set title
		$this->view->title .= ' / ' . $this->view->category['name'];

		// set path
		$this->view->path = array(
			'func' => $this->view->func,
			'id' => (int)$id,
			'page' => (int)$page
		);
	}

	// decide if category is read or unread
	private function statusTopic($read, $time) {
		// guest ? don't bother
		if(!isset($this->session->auth))
			return true;

		// older than one month or has me in array then return as read
		return $time < strtotime("-1 month") || in_array($this->session->auth['id'], unserialize($read)) ? true : false;
	}

	// post new topic
	public function post($id) {
		$this->view->func = 'post';

		// not logged in?
		if(!isset($this->session->auth))
			die(new Login(false, array('type' => 'warning', 'text' => 'Please login before accessing this page')));

		// prepare var
		$id = explode("-", $id, 2);
		$id = $this->db->escapeString((int)reset($id));

		// get category
		$this->view->category = $this->secureCategory($id);
		if(!$this->view->category)
			die(new Error());

		// permission
		if(!isset($this->permissions['create'][$this->view->category['id']]))
			die(new Error());

		// set title
		$this->view->title .= ' / ' . $this->view->category['name'] . ' / New Topic';

		// set path
		$this->view->path = array(
			'func' => $this->view->func,
			'id' => (int)$id
		);
	}

	// get topic securly
	private function secureTopic($id) {
		// get topic
		$topic = $this->db->querySingle("SELECT
			t.id, t.name, t.author, t.views, t.sticky, t.locked, t.read, t.type,
			c.id AS category_id, c.name AS category_name, c.type AS category_type

			FROM topics AS t
			LEFT JOIN categories AS c ON t.category = c.id

			WHERE t.id = '{$id}'
		", true);
		if(!$topic)
			return false;

		// permission
		if(!isset($this->permissions['view'][$topic['category_id']]))
			return false;

		// set topic and category link
		$topic['link'] = $topic['id'] . '-' . Helper::generate_named_id($topic['name']);
		$topic['category_link'] = $topic['category_id'] . '-' . Helper::generate_named_id($topic['category_name']);

		// return category
		return $topic;
	}

	// get replies from topic
	public function topic($id, $page) {
		$this->view->func = 'topic';

		// prepare vars
		$id = explode("-", $id, 2);
		$id = $this->db->escapeString((int)reset($id));
		$page = $this->db->escapeString((int)$page);

		// get topic
		$this->view->topic = $this->secureTopic($id);
		if(!$this->view->topic)
			die(new Error());

		// moderate permission
		$this->view->permission = isset($this->permissions['moderate'][$this->view->topic['category_id']]);

		// increment views
		$views = $this->db->escapeString((int)$this->view->topic['views'] + 1);
		$sql = "views='{$views}'";

		// mark as read
		if(isset($this->session->auth)) {
			$read = unserialize($this->view->topic['read']);
			if(is_array($read)) {
				if(!in_array($this->session->auth['id'], $read))
					$read[] = $this->session->auth['id'];
			} else $read = array($this->session->auth['id']);

			// update read
			$read = $this->db->escapeString(serialize($read));
			$sql .= ", read='{$read}'";
		}

		// update topic
		$this->db->exec("UPDATE topics SET {$sql} WHERE id = '{$this->view->topic['id']}'");

		// count replies
		$count = $this->db->querySingle("SELECT COUNT(id) as nr FROM replies WHERE topic = '{$this->view->topic['id']}'");
		$lastpage = ceil($count / $this->settings['nav_replies']);
		if($page < 1) $page = 1;
		if($page > $lastpage) $page = $lastpage;
		$this->view->pagination = Helper::paginate($lastpage, $page);
		$limit = $count ? ('LIMIT ' .($page - 1) * $this->settings['nav_replies'] .',' . $this->settings['nav_replies']) : null;

		// find the replies
		$result = $this->db->query("SELECT
			r.id, r.message, r.time,
			a.id AS author_id, a.email As author_email, a.name As author_name, x.name AS author_rank

			FROM replies AS r
			LEFT JOIN users AS a ON r.author = a.id
			LEFT JOIN ranks AS x ON a.rank = x.id

		WHERE r.topic = '{$this->view->topic['id']}'
		ORDER BY r.time ASC {$limit}");

		// transform
		$data = array();
		while($res = $result->fetchArray(SQLITE3_ASSOC)) {
			$data[] = array(
				'id' => $res['id'],
				'author_id' => $res['author_id'],
				'author_avatar' => md5(strtolower($res['author_email'])),
				'author_name' => $res['author_name'],
				'author_link' => $res['author_id'] . '-' . Helper::generate_named_id($res['author_name']),
				'author_rank' => $res['author_rank'],
				'time' => Helper::format_time($res['time']),
				'message' => Helper::format_message($res['message'])
			);
		}

		// output data
		$this->view->data = $data;

		// set title
		$this->view->title .= ' / ' . $this->view->topic['category_name'] . ' / ' . $this->view->topic['name'];

		// set path
		$this->view->path = array(
			'func' => $this->view->func,
			'id' => (int)$id,
			'page' => (int)$page
		);
	}

	// find correct page for a post
	public function find() {
		// get from post
		$id = $this->request->post('id', 'int');
		$reply = $this->request->post('reply', 'int');
		if(is_null($id) || is_null($reply))
			return $this->json = array('success' => false);

		// prepare vars
		$id = $this->db->escapeString($id);
		$reply = $this->db->escapeString($reply);

		// get topic
		$topic = $this->secureTopic($id);
		if(!$topic)
			return $this->json = array('success' => false);

		// get the page on where the post is
		$count = $this->db->querySingle("SELECT COUNT(id) as nr FROM replies WHERE topic = '{$id}' AND id <= '{$reply}'");
		$page = ceil($count / $this->settings['nav_replies']);

		// return success
		return $this->json = array(
			'success' => true,
			'link' => $topic['link'] . '/' . $page
		);
	}

	// create the word index
	private function wordIndex($msg, $id = 0) {
		// checks
		if($id == 0)
			return false;

		// more checks
		if(!strlen(trim($msg)) > 0)
			return false;

		// Remove BBCode
		$msg = preg_replace('/\[\/?(b|u|i|quote|img|url)(?:\=[^\]]*)?\]/', ' ', $msg);

		// Remove any apostrophes which aren't part of words
		$msg = substr(preg_replace('((?<=\W)\'|\'(?=\W))', '', ' '.$msg.' '), 1, -1);

		// Remove symbols and multiple whitespace
		$msg = preg_replace('/[\^\$&\(\)<>`"\|,@_\?%~\+\[\]{}:=\/#\\\\;!\*\.\s]+/', ' ', $msg);

		// go through each word
		$t1 = $t2 = null;
		$words = array_unique(explode(' ', strtolower($msg)));
		foreach($words as $word) {
			// word smaller than n letters or bigger than n? abort
			if(strlen($word) < $this->settings['search_min'] || strlen($word) > $this->settings['search_max'])
				continue;

			// found in stop list? abort
			if(in_array($word, explode(', ', STOP_LIST)))
				continue;

			// escape word
			$word = $this->db->escapeString($word);

			// build transaction query
			$t1 .= "INSERT OR IGNORE INTO words (word) VALUES ('{$word}');";
			$t2 .= "INSERT INTO search (reply, word) SELECT '{$id}', id FROM words WHERE word = '{$word}';";
		}

		// run transactions
		if(!is_null($t1)) $this->db->exec("BEGIN TRANSACTION;" . $t1 . "COMMIT;");
		if(!is_null($t2)) $this->db->exec("BEGIN TRANSACTION;" . $t2 . "COMMIT;");
	}

	// clear index for reply
	private function clearIndex($id = 0) {
		// checks
		if($id == 0)
			return false;

		// delete words associated with this reply (except for the ones that are used by other replies)
		$this->db->exec("DELETE FROM words WHERE id IN (
			SELECT w.word
				FROM search AS w
				LEFT JOIN search AS s ON w.word = s.word AND s.reply != '{$id}'
				WHERE w.reply = '{$id}' AND s.reply IS NULL
		)");

		// clear the search
		$this->db->exec("DELETE FROM search WHERE reply = '{$id}'");
	}

	// post new topic
	public function create() {
		// get from post
		$id = $this->request->post('id', 'int');
		$title = $this->request->post('title');
		$message = $this->request->post('message');
		$permission = $this->request->post('permission');
		if(is_null($id) || is_null($title) || is_null($message))
			return $this->json = array('success' => false);

		// prepare vars
		$id = $this->db->escapeString($id);
		$title = $this->db->escapeString($title);
		$message = $this->db->escapeString(Helper::preparse_bbcode($message, $errors));

		// parse errors?
		if(!empty($errors))
			return $this->json = array('success' => false, 'errors' => $errors);

		// not logged in?
		if(!isset($this->session->auth))
			return $this->json = array('success' => false);

		// get category
		$category = $this->secureCategory($id);
		if(!$category)
			return $this->json = array('success' => false);

		// permission (unless we come from curl)
		if(!(!is_null($permission) && $category['type'] == 2))
			if(!isset($this->permissions['create'][$category['id']]))
				return $this->json = array('success' => false);

		// write topic
		$read = $this->db->escapeString(serialize(array($this->session->auth['id'])));
		$this->db->exec("INSERT INTO topics (category, name, author, views, sticky, locked, type, read)
			VALUES ('{$id}', '{$title}', '{$this->session->auth['id']}', '0', '0', '0', '0', '{$read}')");

		// write reply
		$topic = $this->db->lastInsertRowID();
		$this->db->exec("INSERT INTO replies (topic, starter, author, message, time)
			VALUES ('{$topic}', '1', '{$this->session->auth['id']}', '{$message}', '". time() ."')");

		// build word index
		$reply = $this->db->lastInsertRowID();
		$this->wordIndex($title . ' ' . $message, $reply);

		// return success + topic link
		return $this->json = array('success' => true,
			'id' => $reply,
			'name' => $this->session->auth['name'],
			'link' => $topic . '-' . Helper::generate_named_id($title),
			'announce' => $this->announce(__METHOD__, array($this->session->auth['id'], $category['id'], $topic))
		);
	}

	// post a reply
	public function reply() {
		// get from post
		$id = $this->request->post('id', 'int');
		$message = $this->request->post('message');
		if(is_null($id) || is_null($message))
			return $this->json = array('success' => false);

		// prepare vars
		$id = $this->db->escapeString($id);
		$message = $this->db->escapeString(Helper::preparse_bbcode($message, $errors));

		// parse errors?
		if(!empty($errors))
			return $this->json = array('success' => false, 'errors' => $errors);

		// not logged in?
		if(!isset($this->session->auth))
			return $this->json = array('success' => false);

		// get topic
		$topic = $this->secureTopic($id);
		if(!$topic)
			return $this->json = array('success' => false);

		// permission
		if($topic['locked'] == 1 && !isset($this->permissions['moderate'][$topic['category_id']]))
			return $this->json = array('success' => false);

		// mark as unread
		$read = $this->db->escapeString(serialize(array($this->session->auth['id'])));
		$this->db->exec("UPDATE topics SET read='{$read}' WHERE id = '{$topic['id']}'");

		// write reply
		$this->db->exec("INSERT INTO replies (topic, starter, author, message, time)
			VALUES ('{$topic['id']}', '0', '{$this->session->auth['id']}', '{$message}', '". time() ."')");

		// build word index
		$reply = $this->db->lastInsertRowID();
		$this->wordIndex($message, $reply);

		// get topic last page
		$count = $this->db->querySingle("SELECT COUNT(id) as nr FROM replies WHERE topic = '{$topic['id']}'");
		$lastpage = ceil($count / $this->settings['nav_replies']);

		// return success + latest page link
		return $this->json = array(
			'success' => true,
			'id' => $reply,
			'name' => $this->session->auth['name'],
			'link' => $topic['link'] . ($lastpage != 1 ? '/' . $lastpage : ''),
			'announce' => $this->announce(__METHOD__, array($this->session->auth['id'], $topic['category_id'], $topic['id']), array(
				'mod' => '+',
				'reply_id' => $reply,
				'last_page' => $topic['link'] . ($lastpage != 1 ? '/' . $lastpage : '')
			))
		);
	}

	// edit a reply
	public function edit() {
		// get from post
		$id = $this->request->post('id', 'int');
		$message = $this->request->post('message');
		if(is_null($id) || is_null($message))
			return $this->json = array('success' => false);

		// prepare vars
		$id = $this->db->escapeString($id);
		$message = $this->db->escapeString(Helper::preparse_bbcode($message, $errors));

		// parse errors?
		if(!empty($errors))
			return $this->json = array('success' => false, 'errors' => $errors);

		// get reply
		$result = $this->db->querySingle("SELECT id, topic, author, message FROM replies WHERE id = '{$id}'", true);
		if(!$result)
			return $this->json = array('success' => false);

		// not logged in?
		if(!isset($this->session->auth))
			return $this->json = array('success' => false);

		// get topic
		$topic = $this->secureTopic($result['topic']);
		if(!$topic)
			return $this->json = array('success' => false);

		// we have moderate permission or is the reply mine
		if(!(isset($this->permissions['moderate'][$topic['category_id']]) || ($topic['locked'] == 0 && $result['author'] == $this->session->auth['id'])))
			return $this->json = array('success' => false);

		// update reply
		$this->db->exec("UPDATE replies SET message='{$message}' WHERE id = '{$id}'");

		// build word index
		$this->clearIndex($id);
		$this->wordIndex($message, $id);

		// get the edited reply
		$result = $this->db->querySingle("SELECT id, message FROM replies WHERE id = '{$id}'", true);

		// return success
		return $this->json = array(
			'success' => true,
			'name' => $this->session->auth['name'],
			'message' => Helper::format_message($result['message']),
			'announce' => $this->announce(__METHOD__, array($this->session->auth['id'], $topic['category_id'], $topic['id']), array(
				'mod' => '~',
				'reply_id' => $result['id']
			))
		);
	}

	// delete a reply
	public function delete() {
		// get from post
		$id = $this->request->post('id', 'int');
		if(is_null($id))
			return $this->json = array('success' => false);

		// prepare vars
		$id = $this->db->escapeString($id);

		// get reply
		$result = $this->db->querySingle("SELECT id, topic, starter FROM replies WHERE id = '{$id}'", true);
		if(!$result)
			return $this->json = array('success' => false);

		// not logged in?
		if(!isset($this->session->auth))
			return $this->json = array('success' => false);

		// get topic
		$topic = $this->secureTopic($result['topic']);
		if(!$topic)
			return $this->json = array('success' => false);

		// we have moderate permission
		if(!isset($this->permissions['moderate'][$topic['category_id']]))
			return $this->json = array('success' => false);

		// what did we delete?
		$what = $result['starter'] == 1 ? 'topic' : 'reply';

		// delete whole topic?
		if($what == 'topic') {
			// find the replies
			$result = $this->db->query("SELECT id FROM replies WHERE topic = '{$topic['id']}'");
			while($res = $result->fetchArray(SQLITE3_ASSOC)) {
				// delete each reply and clear index
				$this->db->exec("DELETE FROM replies WHERE id = '{$res['id']}'");
				$this->clearIndex($res['id']);
			}

			// now delete the topic
			$this->db->exec("DELETE FROM topics WHERE id = '{$topic['id']}'");

			// return array
			$return = array('link' => 'category/' . $topic['category_link']);
			$announce_data = array('mod' => '!', 'back_page' => $topic['category_link']);

		// no, delete just 1 reply
		} else {
			// delete reply and clear index
			$this->db->exec("DELETE FROM replies WHERE id = '{$result['id']}'");
			$this->clearIndex($id);

			// return array
			$return = array('link' => 'topic/' . $topic['link']);
			$announce_data = array('mod' => '-', 'reply_id' => $result['id']);
		}

		// return success
		return $this->json = array_merge(array(
			'success' => true,
			'name' => $this->session->auth['name'],
			'announce' => $this->announce(__METHOD__, array($this->session->auth['id'], $topic['category_id'], $topic['id']), $announce_data)
		), $return);
	}

	// get reply (for quote / edit)
	private function get() {
		// get from post
		$id = $this->request->post('id', 'int');
		if(is_null($id))
			return $this->json = array('success' => false);

		// prepare vars
		$id = $this->db->escapeString($id);

		// get reply
		$result = $this->db->querySingle("SELECT r.id, r.topic, r.message, r.author, a.name
			FROM replies AS r
			LEFT JOIN users AS a ON r.author = a.id
			WHERE r.id = '{$id}'
		", true);
		if(!$result)
			return $this->json = array('success' => false);

		// not logged in?
		if(!isset($this->session->auth))
			return $this->json = array('success' => false);

		// get topic
		$topic = $this->secureTopic($result['topic']);
		if(!$topic)
			return $this->json = array('success' => false);

		// permissions
		if(!(isset($this->permissions['moderate'][$topic['category_id']]) || $topic['locked'] == 0))
			return $this->json = array('success' => false);

		// send message and name then
		return $this->json = array('success' => true, 'id' => $result['id'], 'author' => $result['name'], 'message' => $result['message']);
	}

	// get topic if we can moderate it
	private function moderate() {
		// get from post
		$id = $this->request->post('id', 'int');
		if(is_null($id))
			return false;

		// not logged in?
		if(!isset($this->session->auth))
			return false;

		// get topic
		$id = $this->db->escapeString($id);
		$topic = $this->secureTopic($id);
		if(!$topic)
			return false;

		// permissions
		if(!isset($this->permissions['moderate'][$topic['category_id']]))
			return false;

		// hooray
		return $topic;
	}

	// stick topic
	public function stick() {
		// get topic
		$topic = $this->moderate();
		if(!$topic)
			return $this->json = array('success' => false);

		// set as sticky
		$this->db->exec("UPDATE topics SET sticky='1' WHERE id = '{$topic['id']}'");
		return $this->json = array(
			'success' => true,
			'noty' => 'Topic is now sticky!',
			'announce' => $this->announce(__METHOD__, array($this->session->auth['id'], $topic['category_id'], $topic['id']))
		);
	}

	// unstick topic
	public function unstick() {
		// get topic
		$topic = $this->moderate();
		if(!$topic)
			return $this->json = array('success' => false);

		// set as no longer sticky
		$this->db->exec("UPDATE topics SET sticky='0' WHERE id = '{$topic['id']}'");
		return $this->json = array(
			'success' => true,
			'noty' => 'Topic is no longer sticky!',
			'announce' => $this->announce(__METHOD__, array($this->session->auth['id'], $topic['category_id'], $topic['id']))
		);
	}

	// lock topic
	public function lock() {
		// get topic
		$topic = $this->moderate();
		if(!$topic)
			return $this->json = array('success' => false);

		// set as locked
		$this->db->exec("UPDATE topics SET locked='1' WHERE id = '{$topic['id']}'");
		return $this->json = array(
			'success' => true,
			'noty' => 'Topic is now locked!',
			'announce' => $this->announce(__METHOD__, array($this->session->auth['id'], $topic['category_id'], $topic['id']))
		);
	}

	// unlock topic
	public function open() {
		// get topic
		$topic = $this->moderate();
		if(!$topic)
			return $this->json = array('success' => false);

		// set as unlocked
		$this->db->exec("UPDATE topics SET locked='0' WHERE id = '{$topic['id']}'");
		return $this->json = array(
			'success' => true,
			'noty' => 'Topic is now unlocked!',
			'announce' => $this->announce(__METHOD__, array($this->session->auth['id'], $topic['category_id'], $topic['id']))
		);
	}

	// accept application
	public function accept() {
		// get topic
		$topic = $this->moderate();
		if(!$topic)
			return $this->json = array('success' => false);

		// notify user
		$tokens = $this->notify('accepted.txt', $topic);
		if($tokens === false)
			return $this->json = array('success' => false);

		// set as accepted
		$this->db->exec("UPDATE topics SET type='1' WHERE id = '{$topic['id']}'");
		return $this->json = array(
			'success' => true,
			'tokens' => $tokens,
			'noty' => 'Application was accepted',
			'warn' => 'User was notified!',
			'announce' => $this->announce(__METHOD__, array($this->session->auth['id'], $topic['category_id'], $topic['id']))
		);
	}

	// decline application
	public function decline() {
		// get topic
		$topic = $this->moderate();
		if(!$topic)
			return $this->json = array('success' => false);

		// notify user
		$tokens = $this->notify('declined.txt', $topic);
		if($tokens === false)
			return $this->json = array('success' => false);

		// set as declined
		$this->db->exec("UPDATE topics SET type='2' WHERE id = '{$topic['id']}'");
		return $this->json = array(
			'success' => true,
			'tokens' => $tokens,
			'noty' => 'Application was declined',
			'warn' => 'User was notified!',
			'announce' => $this->announce(__METHOD__, array($this->session->auth['id'], $topic['category_id'], $topic['id']))
		);
	}

	// notify user for application
	public function notify($tpl, $topic) {
		// category is totally wrong?
		if($topic['category_type'] != 2)
			return false;

		// get author
		$user = $this->db->querySingle("SELECT id, name, rank, email, password FROM users WHERE id = '{$topic['author']}'", true);
		if(!$user)
			return false;

		// default
		$update = false;
		$tokens = array();

		// on accept if user is recruit then promote him
		if($tpl == 'accepted.txt' && $user['rank'] == $this->settings['rank_apply'])
			$update = $this->db->escapeString($this->settings['rank_promote']);

		// on decline if user is member then demote him
		if($tpl == 'declined.txt' && $user['rank'] == $this->settings['rank_promote'])
			$update = $this->db->escapeString($this->settings['rank_apply']);

		// on rank update
		if($update) {
			$this->db->exec("UPDATE users SET rank = '{$update}' WHERE id = '{$user['id']}'");
			$tokens = array(base64_encode(strlen($user['id']) . $user['id'] . $user['password']));
		}

		// notify user
		$eml = Helper::load_email($tpl, $user['name'], $this->settings['blizz_guild'], URL . 'forum/topic/' . $topic['link'], $this->settings['blizz_guild']);
		if($eml)
			$this->email->send($user['email'], $user['name'], $eml);

		// hooray
		return $tokens;
	}

	// announce (params: author_id, category_id, topic_id)
	private function announce($name, $params, $data = false) {
		$locs = array();

		// get announcements category id
		$announcement = $this->db->querySingle("SELECT id FROM categories WHERE type = 1 ORDER BY position ASC");

		// announce separate locations based on functions
		switch($name) {
			// on topic create
			case 'APP\Ctrl\Forum::create':
				// index only if we created a topic in announcements
				if($params[1] == $announcement)
					$locs[] = 'index' . '@' . http_build_query(array('func' => 'index', 'page' => 1));

				// forum
				$locs[] = 'forum' . '@' . http_build_query(array('func' => 'index'));
				$locs[] = 'forum' . '@' . http_build_query(array('func' => 'category', 'id' => $params[1], 'page' => 1));

				// profile
				$locs[] = 'profile' . '@' . http_build_query(array('func' => 'view', 'id' => $params[0]));
			break;

			// on post reply edit delete or topic delete
			case 'APP\Ctrl\Forum::reply':
			case 'APP\Ctrl\Forum::edit':
			case 'APP\Ctrl\Forum::delete':
				// index only if we created a topic in announcements
				if($params[1] == $announcement)
					$locs[] = 'index' . '@' . http_build_query(array('func' => 'index', 'page' => 1));

				// forum
				$locs[] = 'forum' . '@' . http_build_query(array('func' => 'index'));
				$locs[] = 'forum' . '@' . http_build_query(array('func' => 'category', 'id' => $params[1], 'page' => 1));
				$locs[] = 'forum' . '@' . http_build_query(array('func' => 'topic', 'id' => $params[2], 'page' => '_'));

				// profile
				$locs[] = 'profile' . '@' . http_build_query(array('func' => 'view', 'id' => $params[0]));
			break;

			// on manage topic
			case 'APP\Ctrl\Forum::stick':
			case 'APP\Ctrl\Forum::unstick':
			case 'APP\Ctrl\Forum::lock':
			case 'APP\Ctrl\Forum::open':
			case 'APP\Ctrl\Forum::accept':
			case 'APP\Ctrl\Forum::decline':
				// forum
				$locs[] = 'forum' . '@' . http_build_query(array('func' => 'category', 'id' => $params[1], 'page' => 1));
			break;
		}

		// return locations and data
		return array('locations' => $locs, 'data' => $data);
	}
}